/*  This file is Copyright 2010 Hampton Smith
 * 
 *  This file is part of Adventure! a text-adventure game engine by Hampton
 *  Smith.
 *
 *  Adventure! is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Adventure! is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Adventure!.  If not, see http://www.gnu.org/licenses/.
 */

package edu.clemson.cs.hamptos.adventure;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import testadventure.BaseLocation;
import testadventure.TheAdventure;

/**
 * <p>The <code>AdventureEngine</code> does the gruntwork of the game logic.
 * Each <code>AdventureWindow</code> has a single engine associated with it that
 * takes care of the game logic.  This engine forms the bridge between the
 * <code>AdventureParser</code>, which interprets commands, and the game
 * objects such as <code>AdventureLocation</code> that form the basis of the
 * game logic.  It does this by routing commands appropriately to game objects
 * as described below.</p>
 *
 * <p><strong>Routing algorithm:</strong>  Be sure to read the class comments
 * of <code>AdventureParser</code> for a discussion of the parts of a command.
 * Commands that consist only of a <em>verb</em> are routed to the player's
 * current location by calling that location's <code>doCommand()</code> method.
 * If the location should throw a <code>DoNotUnderstandException</code>, the
 * engine will attempt to interpret the command itself.  The engine understands
 * "look"/"examine" type commands, and can provide the user with her
 * "inventory".</p>
 *
 * <p>So, the command <code>"wait"</code> would be routed to the player's
 * current location to be processed.</p>
 *
 * <p>Commands that have a <em>direct object</em> but no
 * <em>indirect object</em> are routed to the direct object's
 * <code>doCommandTo()</code> method.  So the command <code>"take torch"</code>
 * would be routed to the torch object's <code>doCommandTo()</code> method.</p>
 *
 * <p>Commands that have an <em>indirect object</em> in addition to a
 * direct object are routed to the indirect object's
 * <code>doCommandWith()</code> method.  So the command
 * <code>"unlock door with key"</code> would be routed to the key's
 * <code>doCommandWith()</code> method.</p>
 */
public class AdventureEngine {

    //We're using the Strategy design pattern here.  This maps verbs to
    //strategies
    private static final Map<String, VerbStrategy> myGlobalCommands =
            new HashMap<String, VerbStrategy>();

    private final AdventureWindow myWindow;
    private AdventureParser myParser;
    
    private static String myGameTitleText;
    private static File myGameTitleFile;

    private boolean myDebugFlag = false;

    private AdventureLocation myPlayerLocation;
    private AdventureLocation myPlayerInventory = new BaseLocation();
    
    private Integer evil = -1;

    /**
     * <p>Creates a new <code>AdventureEngine</code> that outputs to the
     * given <code>AdventureWindow</code> and uses <code>p</code> to parse
     * input.  The player begins at <code>initialLocation</code>.</p>
     *
     * @param w The input/output window.
     * @param p The parser for input.
     * @param initialLocation The location where the player starts.
     */
    public AdventureEngine(AdventureWindow w, AdventureParser p,
            AdventureLocation initialLocation) {

        if (myGlobalCommands.isEmpty()) {
            setUpGlobalCommands();
        }

        myWindow = w;
        myParser = p;
        
        try {
            myGameTitleFile = new File("./ScriptGameTitle.txt");
            myGameTitleText = TheAdventure.ripStringFromFile(myGameTitleFile);
            myGameTitleText = myGameTitleText.replace(" ", "`");
            
            w.println("<code><b><font size='1' color='red'>" + myGameTitleText +
                    "</font></b></code>");
        } catch (Exception ex)
        {
            System.err.println("Could not initialize the files, "
                               + "check to make sure they are there");
            System.err.println("Error thrown: " + ex.getMessage());
        }
                
        setPlayerLocation(initialLocation);
    }

    /**
     * <p>Turns debug mode on or off.  When debug mode is on, debug statements
     * are printed on certain events, such as when an item is added to a
     * player's inventory.</p>
     *
     * @param debug <code>true</code> if debug mode should be on.
     */
    public void setDebugMode(boolean debug) {
        myDebugFlag = debug;
    }

    private static void setUpGlobalCommands() {
        myGlobalCommands.put("look", new VerbLook());
        myGlobalCommands.put("inventory", new VerbInventory());
        myGlobalCommands.put("take", new VerbTake());
        myGlobalCommands.put("drop", new VerbDrop());
        myGlobalCommands.put("pick", new VerbPick());
        myGlobalCommands.put("no go", new VerbNoGo());
        myGlobalCommands.put("flee now", new VerbNoGo());
        myGlobalCommands.put("game over", new VerbGameOver());
    }

    /**
     * <p>Returns as a <code>Set</code> the a copy of the player's current
     * inventory.  The set is a copy and so changes to it will not affect the
     * underlying inventory.  The objects themselves are not copies, so changes
     * to them will be reflected in the game.</p>
     *
     * @return The player's inventory.
     */
    public Set<AdventureTarget> getPlayerInventory() {
        return myPlayerInventory.getLocalTargets();
    }

    /**
     * <p>Returns <code>true</code> if the player's inventory contains
     * <code>t</code>.  The test for containment is based on
     * <code>t</code>'s <code>equals()</code> and <code>hashcode()</code>
     * methods.  If these methods are not implemented for <code>t</code>, this
     * returns whether or not the specific object <code>t</code> is in the
     * player's inventory.</p>
     *
     * @param t The object to test to see if it's in the player's inventory.
     * @return Whether or not the object is in the player's inventory.
     */
    public boolean playerInventoryContains(AdventureTarget t) {
        return myPlayerInventory.containsLocalTarget(t);
    }
    
    /**
     * <p>Returns the <code>AdventureTarger</code> if the player's inventory contains
     * <code>t</code>. Else, it throws an ItemNotFoundException.
     *
     * @param t The object's string representation to test to see if it's in 
     *    the player's inventory.
     * @return The AdventureTarget found in the inventory
     */
    public AdventureTarget playerInventoryContains(String t) throws NoSuchTargetException {
        return myPlayerInventory.containsLocalTarget(t);
    }

    /**
     * <p>Adds the given <code>AdventureTarget</code> into the player's
     * inventory.</p>
     *
     * @param t The target to add.
     */
    public void addToPlayerInventory(AdventureTarget t) {
        if (t == null) {
            throw new IllegalArgumentException("Cannot add a null target to " +
                    "the player's inventory.");
        }

        if (myPlayerInventory.containsLocalTarget(t)) {
            throw new IllegalStateException(t + " is already in the player's " +
                    "inventory.");
        }

        myPlayerInventory.addLocalTarget(t);
        myPlayerLocation.removeLocalTarget(t);

        debug(t + " added to player's inventory.");
    }

    /**
     * <p>Removes the given target from the player's inventory.</p>
     *
     * @param t The target to remove.
     */
    public void removeFromPlayerInventory(AdventureTarget t) {
        if (!myPlayerInventory.containsLocalTarget(t)) {
            throw new IllegalStateException(t + " is not in the player's " +
                    "inventory.");
        }

        myPlayerInventory.removeLocalTarget(t);
        myPlayerLocation.addLocalTarget(t);

        debug(t + " removed from player's inventory.");
    }

    /**
     * <p>Moves the player from his current location to <code>l</code>, printing
     * <code>l</code>'s description.</p>
     *
     * @param l The location to which the player should be moved.
     */
    public void setPlayerLocation(AdventureLocation l) {
        if (l == null) {
            throw new IllegalArgumentException("Cannot move the player to a " +
                    "null location.");
        }

        debug("Player moves to " + l + ".");

        myPlayerLocation = l;
        
        l.notifyListeners(this, myWindow);
        
        myWindow.println(l.getDescription());
    }

    /**
     * <p>Returns the <code>AdventureLocation</code> that currently houses
     * the player.</p>
     *
     * @return The <code>AdventureLocation</code> in which the player currently
     *     resides.
     */
    public AdventureLocation getPlayerLocation() {
        return myPlayerLocation;
    }

    /**
     * <p>Attempts to execute <code>command</code> in the current location.</p>
     *
     * @param command The command to execute.
     */
    void doCommand(String command) {

        myWindow.println("\n<font color='#0000DD'><i>" + command +
                "</i></font>");

        debug("Parsing command: " + command);

        try {
            AdventureCommand parsedCommand = myParser.parse(command, this);
            doCommand(parsedCommand);
        }
        catch (IllegalArgumentException e) {
            myWindow.println("\nI don't understand \"" + command + "\".");
        }
        catch (NoSuchTargetException e) {
            myWindow.println("\nI don't see a \"" + e.getItemName() +
                    "\" here...\n");
        }
        
    }

    private void doCommand(AdventureCommand parsedCommand) {
        String verb = parsedCommand.getVerb();
        AdventureTarget directObject = parsedCommand.getDirectObject();
        AdventureTarget indirectObject = parsedCommand.getIndirectObject();

        debug("Command(verb = " + verb + ", direct = " + directObject +
                ", indirect = " + indirectObject + ")");

        if (verb == null) {
            throw new GameEngineException("Parser returned bad verb: null");
        }

        if (verb.length() == 0) {
            throw new GameEngineException("Parser returned bad verb: \"\"");
        }

        try {
            if (directObject == null) {
                if (indirectObject == null) {

                    debug("Command routed to current location.");

                    myPlayerLocation.doCommand(parsedCommand, this, myWindow);
                }
                else {

                    debug("Command routed to indirect object.");

                    indirectObject.doCommandWith(parsedCommand, this, myWindow);
                }
            }
            else {
                AdventureTarget commandTarget;

                if (indirectObject == null) {
                    
                    debug("Command routed to direct object.");

                    directObject.doCommandTo(parsedCommand, this, myWindow);
                }
                else {

                    debug("Command routed to indirect object.");

                    indirectObject.doCommandWith(parsedCommand, this, myWindow);
                }

                
            }
        }
        catch (DoNotUnderstandException e) {
            try {
                interpretCommand(parsedCommand);
            }
            catch (DoNotUnderstandException e2) {
                //Nobody knows how to do this command!

                printDontKnowHowError(e);
            }
        }
        checkEvil();
    }

    private void printDontKnowHowError(DoNotUnderstandException e) {
        debug("DoNotUnderstandException.  message = " + e.getMessage());

        String message = e.getMessage();

        if (message == null) {
            AdventureCommand c = e.getCommand();

            if (c.getIndirectObject() != null) {
                message = "I don't know how to use the " +
                        c.getIndirectObjectInvocation() + " to " + c.getVerb() +
                        "!";
            }
            else if (c.getDirectObject() != null) {
                message = "I don't know how to " + c.getVerb() + " the " +
                        c.getDirectObjectInvocation() + "!";
            }
            else {
                message = "I don't know how to " + c.getVerb() + "!";
            }
        }

        myWindow.println("\n" + message);
    }

    private void interpretCommand(AdventureCommand c)
            throws DoNotUnderstandException {
        
        String verb = c.getVerb();

        if (myGlobalCommands.containsKey(verb)) {
            VerbStrategy s = myGlobalCommands.get(verb);
            s.doCommand(c, this, myWindow);
        }
        else {
            throw new DoNotUnderstandException(c);
        }
    }

    private void debug(String message) {
        if (myDebugFlag) {
            myWindow.println("\n<font color='#666666'><b>DEBUG</b> -- " + 
                    message + "</font>\n");
        }
    }

    /**
     * <p>Prevents any more actions by the player by uprooting the current parser</p>
     */
    public void gameOver() {
        myWindow.println("OH NOES!!! UR DEAD!!!");
        myWindow.println("The zombies gotcha, better luck next time.");
        myParser = new GameOverParser();
    }

    /**
     * <p>Starts the player death countdown</p>
     * @param get 
     */
    public void startEvil(Integer get) {
        evil = get;
    }
    
    /**
     * <p>Stops the player death countdown</p>
     */
    public void stopEvil()
    {
        evil = -1;
    }
    
    /**
     * <p>Checks the countdown until the player dies and decrements</p>
     */
    private void checkEvil()
    {
        if (evil > 0)
        {
            evil--;
        }
        else if(evil == 0)
        {
            gameOver();
            evil--;
        }
    }

    /**
     * <p>Triggers the ending of the adventure by replacing the parser</p>
     * @param pwd 
     */
    public void triggerEnding(int pwd) {
        myParser = new EndParser(pwd);
        startEvil(3);
    }
}
